/**
 * 
 */
package org.simplextensions.registry;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.simplextensions.annotations.Depends;
import org.simplextensions.annotations.ExtensionStyle;
import org.simplextensions.annotations.ExtensionValidator;
import org.simplextensions.annotations.ExtensionValidators;
import org.simplextensions.configuration.BundleConfiguration;
import org.simplextensions.di.DependencyInjector;
import org.simplextensions.graph.Graph;
import org.simplextensions.graph.GraphEvent;
import org.simplextensions.graph.GraphEventListenerAdapter;
import org.simplextensions.graph.GraphException;
import org.simplextensions.registry.exceptions.ExtensionsConfigurationException;
import org.simplextensions.registry.exceptions.ExtensionsInstantiationsException;
import org.simplextensions.registry.phaselisteners.DefaultExtensionClassStructurePhaseListener;
import org.simplextensions.registry.phaselisteners.DependenciesScanPhaseListener;
import org.simplextensions.registry.phaselisteners.DependencyLocatorScanPhaseListener;
import org.simplextensions.registry.phaselisteners.ExtensionEvent;
import org.simplextensions.registry.phaselisteners.ExtensionPointEvent;
import org.simplextensions.registry.phaselisteners.ExtensionPointScanPhaseListener;
import org.simplextensions.registry.phaselisteners.ExtensionPointsCreationPhaseListeners;
import org.simplextensions.registry.phaselisteners.ExtensionScanPhaseListener;
import org.simplextensions.registry.phaselisteners.ICreationPhaseListener;
import org.simplextensions.registry.phaselisteners.IProcessingPhaseListener;
import org.simplextensions.registry.phaselisteners.IRegisterPhaseListener;
import org.simplextensions.registry.phaselisteners.IValidationPhaseListener;
import org.simplextensions.registry.phaselisteners.PropertiesScanPhaseListener;
import org.simplextensions.registry.phaselisteners.RegisterPhaseListener;
import org.simplextensions.registry.phaselisteners.ServiceScanPhaseListener;
import org.simplextensions.registry.phaselisteners.ValidationPhaseListener;
import org.simplextensions.registry.phaselisteners.ValidatorsScanPhaseListener;
import org.simplextensions.scanner.ClasspathScanner;
import org.simplextensions.scanner.ClasspathScannerException;

/**
 * @author Tomasz Krzyzak, <a
 *         href="mailto:tomasz.krzyzak@gmail.com">tomasz.krzyzak@gmail.com</a>
 * @since 2009-08-04 10:29:32
 * 
 */
public class ExtensionRegistry implements IExtensionRegistry {

	private static final Log log = LogFactory.getLog(ExtensionRegistry.class);

	Map<String, ExtensionPoint> extensionPointsMap = new HashMap<String, ExtensionPoint>();
	Set<Extension> extensions = new HashSet<Extension>();

	private ClasspathScanner classPathScanner;

	private List<IRegisterPhaseListener> registerListeners = new LinkedList<IRegisterPhaseListener>();

	private List<ICreationPhaseListener> creationListeners = new LinkedList<ICreationPhaseListener>();

	private List<IValidationPhaseListener> validationListeners = new LinkedList<IValidationPhaseListener>();

	private Graph graph = new Graph();

	private boolean started = false;

	private final Collection<Bundle> bundles = new LinkedList<Bundle>();

	private boolean lazyDependencyResolving = false;

	private DependencyLocatorRegistry dependencyLocatorRegistry;

	/**
	 * @param serviceRegistry
	 */
	public ExtensionRegistry(Collection<BundleConfiguration> bundles) {
		classPathScanner = new ClasspathScanner();
		// configuring classpath scanner
		log.debug("configuring ClassPathScanner");

		addScannPhaseListener(org.simplextensions.annotations.ExtensionPoint.class, new ExtensionPointScanPhaseListener(), 1);
		addScannPhaseListener(org.simplextensions.annotations.Extension.class, new ExtensionScanPhaseListener(), 1);
		addScannPhaseListener(org.simplextensions.annotations.Service.class, new ServiceScanPhaseListener(), 1);
		addScannPhaseListener(org.simplextensions.annotations.DependencyLocator.class, new DependencyLocatorScanPhaseListener(), 1);

		addScannPhaseListener(org.simplextensions.annotations.ExtensionPoint.class, new PropertiesScanPhaseListener(), 2);
		addScannPhaseListener(org.simplextensions.annotations.Extension.class, new PropertiesScanPhaseListener(), 2);
		addScannPhaseListener(org.simplextensions.annotations.Properties.class, new PropertiesScanPhaseListener(), 2);
		addScannPhaseListener(Depends.class, new DependenciesScanPhaseListener(), 2);
		addScannPhaseListener(ExtensionValidator.class, new ValidatorsScanPhaseListener(), 2);
		addScannPhaseListener(ExtensionValidators.class, new ValidatorsScanPhaseListener(), 2);

		addRegisterPhaseListener(new DefaultExtensionClassStructurePhaseListener());
		addRegisterPhaseListener(new RegisterPhaseListener());

		addValidatePhaseListener(new ValidationPhaseListener());

		addCreationPhaseListener(new ExtensionPointsCreationPhaseListeners());
		for (BundleConfiguration bc : bundles) {
			try {
				this.bundles.add(new Bundle(this, bc));
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}

		graph.addGraphListener(new GraphEventListenerAdapter() {

			public void nodeFullyConnected(GraphEvent graphEvent) {
				ConfigurableElem ce = (ConfigurableElem) graphEvent.data;
				ce.setActive(true);
			}

		});
	}

	public boolean isStarted() {
		return started;
	}

	void setStarted(boolean started) {
		this.started = started;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.simplextensions.core.IExtensionRegistry#getExtensionPoint(java.lang
	 * .String)
	 */
	public ExtensionPoint getExtensionPointMD(String extensionPointId) {
		return this.extensionPointsMap.get(extensionPointId);
	}

	public void start() {
		if (isStarted())
			throw new IllegalStateException("Already started");

		log.info("----------------------------------------------");
		log.info("scanning phase");
		log.info("----------------------------------------------");
		for (Bundle bundle : this.bundles) {
			try {
				classPathScanner.scannClassPath(bundle);
			} catch (ClasspathScannerException e1) {
				log.error("Scanning: " + bundle.getName(), e1);
			}
		}

		log.info("----------------------------------------------");
		log.info("structure phase");
		log.info("----------------------------------------------");

		for (ExtensionPoint e : this.extensionPointsMap.values()) {
			processExtensionPoint(e.getBundle().getRegisterListeners(), e);
			validateExtensionPoint(e);
		}
		for (Extension e : this.extensions) {
			processExtension(e.getBundle().getRegisterListeners(), e);
			if (!validateExtension(e)) {
				log.warn("!!!!!!!!!!!! invalid extension: " + e.getId());
			}
		}
		setStarted(true);
		log.debug("initializeTime: " + initializeTime / 1000000.0 + "ms");
	}

	public void addExtensionPoint(ExtensionPoint extensionPoint) {
		log.trace("Adding extensionPoint: " + extensionPoint.getId());
		this.extensionPointsMap.put(extensionPoint.getId(), extensionPoint);

		if (isStarted()) {
			processExtensionPoint(extensionPoint.getBundle().getRegisterListeners(), extensionPoint);
			validateExtensionPoint(extensionPoint);
		}
	}

	private void validateExtensionPoint(ExtensionPoint extensionPoint) {
		extensionPoint.setValid(processExtensionPoint(extensionPoint.getBundle().getValidationListeners(), extensionPoint));
		if (extensionPoint.isValid()) {
			try {
				graph.addNode(extensionPoint.getId(), extensionPoint.getDepends(), extensionPoint);
			} catch (GraphException e) {
				e.printStackTrace();
			}
		} else {
			graph.removeNode(extensionPoint);
		}
	}

	public void addExtension(Extension extension) {
		log.trace("Adding extension: " + extension.getId());
		this.extensions.add(extension);

		if (isStarted()) {
			processExtension(extension.getBundle().getRegisterListeners(), extension);
			validateExtension(extension);
			initializeExtensionPoints();
		}
	}

	private void initializeExtensionPoints() {
		for (ExtensionPoint ep : extensionPointsMap.values()) {
			initialize(ep);
		}
	}

	private boolean validateExtension(Extension extension) {
		if (extension.isValid())
			return true;

		extension.setValid(processExtension(extension.getBundle().getValidationListeners(), extension));
		if (extension.isValid()) {
			try {
				graph.addNode(extension.getFullId(), extension.getDepends(), extension);
			} catch (GraphException e) {
				throw new RuntimeException(e);
			}
			return true;
		} else {
			// log.warn("!!!!!!!!!!!!removin node: " + extension.getId());
			// graph.removeNode(extension);
			return false;
		}

	}

	private long initializeTime = 0;

	private void initialize(ExtensionPoint ep) {

		log.debug("initializing extensionPoint: " + ep.getId());
		if (ep.isActive()) {
			initializeTime -= System.nanoTime();
			ep.getExecutable().initialize(ep);
			ep.setInitialized(true);
			initializeTime += System.nanoTime();
		} else {
			log.warn("Omitting inactive extension point " + ep.getId());
		}
	}

	private boolean processExtension(List<? extends IProcessingPhaseListener> listeners, Extension e) {
		ExtensionEvent event = new ExtensionEvent(this, e);
		for (IProcessingPhaseListener l : listeners) {
			try {
				l.processExtension(event);
			} catch (Exception e2) {
				log.error("", e2);
			}
			if (!event.doit)
				break;
		}
		return event.doit;
	}

	private boolean processExtensionPoint(List<? extends IProcessingPhaseListener> listeners, ExtensionPoint e) {
		log.trace("Process extension point: " + e.getId());
		ExtensionPointEvent event = new ExtensionPointEvent(this, e);
		for (IProcessingPhaseListener l : listeners) {
			try {
				l.processExtensionPoint(event);
			} catch (Exception e2) {
				log.error("", e2);
			}
			if (!event.doit)
				break;
		}
		return event.doit;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.simplextensions.core.IExtensionRegistry#getExtensionPoints()
	 */
	public Collection<ExtensionPoint> getExtensionPointsMD() {
		return Collections.unmodifiableCollection(this.extensionPointsMap.values());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.simplextensions.core.IExtensionRegistry#getExtensions(java.lang.String
	 * )
	 */
	public Collection<Extension> getExtensionsMD(String extensionPointId) {
		ExtensionPoint extensionPoint = this.extensionPointsMap.get(extensionPointId);
		Collection<Extension> result = new LinkedList<Extension>();
		if (extensionPoint != null) {
			for (Extension e : this.extensions)
				if (extensionPoint == e.getExtensionPoint())
					result.add(e);
		}
		return result;
	}

	public Collection<Extension> getExtensionsMDByClass(String name) {
		Collection<Extension> result = new LinkedList<Extension>();
		for (Extension e : this.extensions) {
			if (e.getClassName() != null && e.getClassName().equals(name)) {
				result.add(e);
			}
		}
		return result;
	}

	public Collection<ExtensionPoint> getExtensionPointsMDByClass(String name) {
		Collection<ExtensionPoint> result = new LinkedList<ExtensionPoint>();
		for (ExtensionPoint e : this.extensionPointsMap.values()) {
			if (e.getClassName() != null && e.getClassName().equals(name)) {
				result.add(e);
			}
		}
		return result;
	}

	public Collection<Extension> getActiveExtensions(ExtensionPoint ep) {
		Collection<Extension> extensions = getExtensionsMD(ep.getId());
		Collection<Extension> result = new LinkedList<Extension>();
		for (Extension e : extensions) {
			if (e.isValid()) {
				result.add(e);
			}
		}
		return result;
	}

	private void checkStarted() {
		if (isStarted())
			throw new IllegalStateException("Already started");
	}

	private void addCreationPhaseListener(ICreationPhaseListener listener) {
		checkStarted();

		this.creationListeners.add(listener);
	}

	private void addRegisterPhaseListener(IRegisterPhaseListener listener) {
		checkStarted();

		this.registerListeners.add(listener);
	}

	private Map<Class<?>, Map<Integer, Collection<IExtensionScanPhaseListener>>> scanListenersMap = new HashMap<Class<?>, Map<Integer, Collection<IExtensionScanPhaseListener>>>();

	private void addScannPhaseListener(Class<?> annotationClass, IExtensionScanPhaseListener listener, int level) {
		checkStarted();

		Map<Integer, Collection<IExtensionScanPhaseListener>> map = scanListenersMap.get(annotationClass);
		if (map == null) {
			scanListenersMap.put(annotationClass, map = new HashMap<Integer, Collection<IExtensionScanPhaseListener>>());
		}
		Collection<IExtensionScanPhaseListener> collection = map.get(level);
		if (collection == null) {
			map.put(level, collection = new LinkedList<IExtensionScanPhaseListener>());
		}
		collection.add(listener);
	}

	private void addValidatePhaseListener(IValidationPhaseListener listener) {
		checkStarted();

		this.validationListeners.add(listener);
	}

	public void registerExtension(ExtensionPoint extensionPoint, Extension extension) {
		if (!extension.isRegistered()) {
			extension.setExtensionPoint(extensionPoint);
			validateExtension(extension);
		}
	}

	public Object createExecutable(ConfigurableElem configurableElem) {
		if (!configurableElem.isValid())
			throw new IllegalStateException("cannot create executables of inactive element");

		if (ExtensionStyle.VIRTUAL == configurableElem.getExtensionStyle()) {
			throw new ExtensionsInstantiationsException("cannot instantiate virtual elem");
		}
		if (ExtensionStyle.SINGLETON == configurableElem.getExtensionStyle() && configurableElem.getFactoryMethodName() == null) {
			throw new ExtensionsConfigurationException("style is singleton bu no factory method is provideed");
		}

		log.trace("!!! creating executable: " + configurableElem.getId());

		Object executable;
		if (ExtensionStyle.SINGLETON == configurableElem.getExtensionStyle()) {
			Method method;
			try {
				method = configurableElem.getClazz().getMethod(configurableElem.getFactoryMethodName());
				executable = method.invoke(null);
			} catch (Exception e) {
				throw new ExtensionsInstantiationsException("cannot instantiate elem", e);
			}
		} else {
			try {
				executable = configurableElem.getClazz().newInstance();
			} catch (Exception e) {
				throw new ExtensionsInstantiationsException("cannot instantiate elem", e);
			}
		}
		if (dependencyLocatorRegistry == null) {
			if (!lazyDependencyResolving) {
				lazyDependencyResolving = true;
				dependencyLocatorRegistry = getExtensionPoint(DependencyLocatorRegistry.class);
				resolverExecutableDependency(dependencyLocatorRegistry);
				lazyDependencyResolving = false;

			}
		}
		if (!lazyDependencyResolving) {
			resolverExecutableDependency(executable);
		}
		return executable;
	}

	private void resolverExecutableDependency(Object executable) {
		DependencyInjector.resolveDependency(dependencyLocatorRegistry, executable);

	}

	public List<IRegisterPhaseListener> getRegisterListeners() {
		return Collections.unmodifiableList(registerListeners);
	}

	public List<ICreationPhaseListener> getCreationListeners() {
		return Collections.unmodifiableList(creationListeners);
	}

	public List<IValidationPhaseListener> getValidationListeners() {
		return Collections.unmodifiableList(validationListeners);
	}

	public Map<Class<?>, Map<Integer, Collection<IExtensionScanPhaseListener>>> getScanListeners() {
		return Collections.unmodifiableMap(this.scanListenersMap);
	}

	public IServiceRegistry getServiceRegistry() {
		return getExtensionPoint(IServiceRegistry.class, IService.EP_ID);
	}

	public <T> T getService(Class<T> clazz) {
		IServiceRegistry serviceRegistry = getServiceRegistry();
		return serviceRegistry != null ? serviceRegistry.getService(clazz) : null;
	}

	public <T> T getExtensionPoint(Class<T> clazz) {
		return getExtensionPoint(clazz, null);
	}

	public <T> T getExtensionPoint(Class<T> clazz, String id) {
		ExtensionPoint result = null;
		for (ExtensionPoint ep : this.extensionPointsMap.values()) {
			if (clazz.isAssignableFrom(ep.getClazz()) && (id == null || (id != null && id.equals(ep.getId())))) {
				result = ep;
			}
		}
		if (result != null)
			return (T) result.getExecutable();
		else
			return null;
	}

}
